/**
 * Copyright 2018 jianggujin (www.jianggujin.com).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jianggujin.codec;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;

import com.jianggujin.codec.util.JCipherInputStream;
import com.jianggujin.codec.util.JCipherOutputStream;
import com.jianggujin.codec.util.JCodecException;
import com.jianggujin.codec.util.JCodecUtils;

/**
 * Password-based encryption（基于密码加密）
 * 
 * @author jianggujin
 *
 */
public class JPBE {
   /**
    * 默认迭代次数
    */
   public static final int DEFAULT_ITERATION_COUNT = 100;

   /**
    * PBE算法
    * 
    * @author jianggujin
    *
    */
   public static enum JPBEAlgorithm {
      PBEWithMD5AndDES, PBEWithSHA1AndDESede, PBEWithSHA1AndRC2_40;

      public String getName() {
         return this.name();
      }
   }

   /**
    * 初始化盐
    * 
    * @return
    */
   public static byte[] initSalt() {
      byte[] salt = new byte[8];
      Random random = new Random();
      random.nextBytes(salt);
      return salt;
   }

   /**
    * 加密
    * 
    * @param data
    * @param password
    * @param salt
    * @param algorithm
    * @return
    */
   public static byte[] encrypt(byte[] data, char[] password, byte[] salt, JPBEAlgorithm algorithm) {
      return encrypt(data, password, salt, algorithm.getName());
   }

   /**
    * 加密
    * 
    * @param data
    * @param password
    * @param salt
    * @param algorithm
    * @return
    */
   public static byte[] encrypt(byte[] data, char[] password, byte[] salt, String algorithm) {
      return encrypt(data, password, salt, DEFAULT_ITERATION_COUNT, algorithm);
   }

   /**
    * 加密
    * 
    * @param data
    * @param secretKey
    * @param salt
    * @return
    */
   public static byte[] encrypt(byte[] data, SecretKey secretKey, byte[] salt) {
      return encrypt(data, secretKey, salt, DEFAULT_ITERATION_COUNT);
   }

   /**
    * 加密
    * 
    * @param data
    * @param password
    * @param salt
    * @param iterationCount
    * @param algorithm
    * @return
    */
   public static byte[] encrypt(byte[] data, char[] password, byte[] salt, int iterationCount,
         JPBEAlgorithm algorithm) {
      return encrypt(data, password, salt, iterationCount, algorithm.getName());
   }

   /**
    * 加密
    * 
    * @param data
    * @param password
    * @param salt
    * @param iterationCount
    * @param algorithm
    * @return
    */
   public static byte[] encrypt(byte[] data, char[] password, byte[] salt, int iterationCount, String algorithm) {
      SecretKey secretKey = getSecretKey(password, algorithm);
      return encrypt(data, secretKey, salt, iterationCount);
   }

   /**
    * 加密
    * 
    * @param data
    * @param secretKey
    * @param salt
    * @param iterationCount
    * @return
    */
   public static byte[] encrypt(byte[] data, SecretKey secretKey, byte[] salt, int iterationCount) {
      // 数据加密
      Cipher cipher = getCipher(secretKey, salt, Cipher.ENCRYPT_MODE);
      return JCodecUtils.doFinal(data, cipher);
   }

   public static OutputStream wrap(OutputStream out, char[] password, byte[] salt, JPBEAlgorithm algorithm) {
      return wrap(out, password, salt, algorithm.getName());
   }

   public static OutputStream wrap(OutputStream out, char[] password, byte[] salt, String algorithm) {
      return wrap(out, password, salt, DEFAULT_ITERATION_COUNT, algorithm);
   }

   public static OutputStream wrap(OutputStream out, SecretKey secretKey, byte[] salt) {
      return wrap(out, secretKey, salt, DEFAULT_ITERATION_COUNT);
   }

   public static OutputStream wrap(OutputStream out, char[] password, byte[] salt, int iterationCount,
         JPBEAlgorithm algorithm) {
      return wrap(out, password, salt, algorithm.getName());
   }

   public static OutputStream wrap(OutputStream out, char[] password, byte[] salt, int iterationCount,
         String algorithm) {
      Cipher cipher = getCipher(password, salt, algorithm, Cipher.ENCRYPT_MODE);

      return new JCipherOutputStream(cipher, out);
   }

   public static OutputStream wrap(OutputStream out, SecretKey secretKey, byte[] salt, int iterationCount) {
      Cipher cipher = getCipher(secretKey, salt, Cipher.ENCRYPT_MODE);
      return new JCipherOutputStream(cipher, out);
   }

   /**
    * 解密
    * 
    * @param data
    * @param password
    * @param salt
    * @param algorithm
    * @return
    */
   public static byte[] decrypt(byte[] data, char[] password, byte[] salt, JPBEAlgorithm algorithm) {
      return decrypt(data, password, salt, algorithm.getName());
   }

   /**
    * 解密
    * 
    * @param data
    * @param password
    * @param salt
    * @param algorithm
    * @return
    */
   public static byte[] decrypt(byte[] data, char[] password, byte[] salt, String algorithm) {
      return decrypt(data, password, salt, DEFAULT_ITERATION_COUNT, algorithm);
   }

   /**
    * 解密
    * 
    * @param data
    * @param secretKey
    * @param salt
    * @return
    */
   public static byte[] decrypt(byte[] data, SecretKey secretKey, byte[] salt) {
      return decrypt(data, secretKey, salt, DEFAULT_ITERATION_COUNT);
   }

   /**
    * 解密
    * 
    * @param data
    * @param password
    * @param salt
    * @param iterationCount
    * @param algorithm
    * @return
    */
   public static byte[] decrypt(byte[] data, char[] password, byte[] salt, int iterationCount,
         JPBEAlgorithm algorithm) {
      return decrypt(data, password, salt, iterationCount, algorithm.getName());
   }

   /**
    * 解密
    * 
    * @param data
    * @param password
    * @param salt
    * @param iterationCount
    * @param algorithm
    * @return
    */
   public static byte[] decrypt(byte[] data, char[] password, byte[] salt, int iterationCount, String algorithm) {
      SecretKey secretKey = getSecretKey(password, algorithm);
      return decrypt(data, secretKey, salt, iterationCount);
   }

   /**
    * 解密
    * 
    * @param data
    * @param secretKey
    * @param salt
    * @param iterationCount
    * @return
    */
   public static byte[] decrypt(byte[] data, SecretKey secretKey, byte[] salt, int iterationCount) {
      // 数据解密
      Cipher cipher = getCipher(secretKey, salt, Cipher.DECRYPT_MODE);
      return JCodecUtils.doFinal(data, cipher);
   }

   public static InputStream wrap(InputStream in, char[] password, byte[] salt, JPBEAlgorithm algorithm) {
      return wrap(in, password, salt, algorithm.getName());
   }

   public static InputStream wrap(InputStream in, char[] password, byte[] salt, String algorithm) {
      return wrap(in, password, salt, DEFAULT_ITERATION_COUNT, algorithm);
   }

   public static InputStream wrap(InputStream in, SecretKey secretKey, byte[] salt) {
      return wrap(in, secretKey, salt, DEFAULT_ITERATION_COUNT);
   }

   public static InputStream wrap(InputStream in, char[] password, byte[] salt, int iterationCount,
         JPBEAlgorithm algorithm) {
      return wrap(in, password, salt, iterationCount, algorithm.getName());
   }

   public static InputStream wrap(InputStream in, char[] password, byte[] salt, int iterationCount, String algorithm) {
      Cipher cipher = getCipher(password, salt, algorithm, Cipher.DECRYPT_MODE);
      return new JCipherInputStream(cipher, in);
   }

   public static InputStream wrap(InputStream in, SecretKey secretKey, byte[] salt, int iterationCount) {
      Cipher cipher = getCipher(secretKey, salt, Cipher.DECRYPT_MODE);
      return new JCipherInputStream(cipher, in);
   }

   public static Cipher getCipher(char[] password, byte[] salt, JPBEAlgorithm algorithm, int opmode) {
      return getCipher(password, salt, algorithm.getName(), opmode);
   }

   public static Cipher getCipher(char[] password, byte[] salt, String algorithm, int opmode) {
      return getCipher(password, salt, DEFAULT_ITERATION_COUNT, algorithm, opmode);
   }

   public static Cipher getCipher(char[] password, byte[] salt, int iterationCount, JPBEAlgorithm algorithm,
         int opmode) {
      return getCipher(password, salt, iterationCount, algorithm.getName(), opmode);
   }

   public static Cipher getCipher(char[] password, byte[] salt, int iterationCount, String algorithm, int opmode) {
      SecretKey secretKey = getSecretKey(password, algorithm);
      return getCipher(secretKey, salt, iterationCount, opmode);
   }

   public static Cipher getCipher(SecretKey secretKey, byte[] salt, int opmode) {
      return getCipher(secretKey, salt, DEFAULT_ITERATION_COUNT, opmode);
   }

   public static Cipher getCipher(SecretKey secretKey, byte[] salt, int iterationCount, int opmode) {
      JCodecUtils.checkOpMode(opmode);
      PBEParameterSpec paramSpec = new PBEParameterSpec(salt, iterationCount);
      try {
         // 数据加密
         Cipher cipher = Cipher.getInstance(secretKey.getAlgorithm());
         cipher.init(opmode, secretKey, paramSpec);
         return cipher;
      } catch (Exception e) {
         throw new JCodecException(e);
      }
   }

   public static SecretKey getSecretKey(char[] password, JPBEAlgorithm algorithm) {
      return getSecretKey(password, algorithm.getName());
   }

   public static SecretKey getSecretKey(char[] password, String algorithm) {
      try {
         PBEKeySpec keySpec = new PBEKeySpec(password);
         SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(algorithm);
         return keyFactory.generateSecret(keySpec);
      } catch (Exception e) {
         throw new JCodecException(e);
      }
   }
}
